/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 Tim Leerhoff <tleerhof@web.de>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */


/**
 * Tracksymbol for leaflet.
 * The visualization is chosen by zoomlevel or heading availability.
 * If zoomlevel is smaller than 'minSilouetteZoom' a triangular symbol is rendered.
 * If zoomlevel is greater than 'minSilouetteZoom' a ship silouette is rendered.
 * If heading is undefined a diamond symbol is rendered.
 * The following options are available:
 * <ul>
 *   <li>trackId: The unique id of the symbol (default: 0). </li>
 *   <li>size: Static size of the symbol in pixels (default:24). </li>
 *   <li>heading: Initial heading of the symbol (default: undefined). </li>
 *   <li>course: Initial course of the symbol (default: undefined). </li>
 *   <li>speed: Initial speed of the symbol-leader (default: undefined). </li>
 *   <li>leaderTime: The length of the leader (speed * leaderTime) (default:60s). </li>
 *   <li>minSilouetteZoom: The zoomlevel to switch from triangle to silouette (default:13). </li>
 *   <li>gpsRefPos: Initial GPS offset of the symbol (default: undefined). </li>
 *   <li>defaultSymbol: The triangular track symbol. Contains an array of n numbers. [x1,y1,x2,y2,...] </li>
 *   <li>noHeadingSymbol: The diamond track symbol. Contains an array of n numbers. [x1,y1,x2,y2,...] </li>
 *   <li>silouetteSymbol: The ship track symbol. Contains an array of n numbers. [x1,y1,x2,y2,...] </li>
 * </ul>
 * @class TrackSymbol
 * @constructor
 * @param latlng {LanLng} The initial position of the symbol.
 * @param options {Object} The initial options described above.
 */
L.TrackSymbol = L.Path.extend({

  initialize: function (latlng, options) {
    L.setOptions(this, options);
    if (latlng === undefined) {
      throw Error('Please give a valid lat/lon-position');
    }
    options = options || {};
    this._id = options.trackId || 0;

    // if (this._id === '413438070') {
    //   console.log('marker 38070:', options);
    // }

    this._leaflet_id = this._id + 'M';
    this._latlng = L.latLng(latlng);
    this._size = options.size || 24;
    this._course = options.course * Math.PI / 180;
    if (!!options.heading && options.heading > -1) {
      this._heading = options.heading * Math.PI / 180;
    } else {
      this._heading = this._course;
    }
    // this._heading = options.heading * Math.PI / 180;
    // if (this._heading < 0) {
    //   this._heading = this._course;
    // }
    this._speed = options.speed;
    this._leaderTime = options.leaderTime || 60.0;
    this._minSilouetteZoom = options.minSilouetteZoom || 111;
    this.setGPSRefPos(options.gpsRefPos);
    this._triSymbol = options.defaultSymbol || [0.75, 0, -0.25, 0.3, -0.25, -0.3];
    this._diaSymbol = options.noHeadingSymbol || [0.3, 0, 0, 0.3, -0.3, 0, 0, -0.3];
    this._silSymbol = options.silouetteSymbol || [1, 0.5, 0.75, 1, 0, 1, 0, 0, 0.75, 0];
    this._focus = false;
    // if (this._id === '413325550') {
    //   console.log(options.heading, options.course);
    // }
    // if (this._id === '413438050') {
    //   console.log('heading:::::', options);
    // }
    // if (this._id === '414301000') {
    //   console.log('init:', this._course, this._heading);
    // }
  },

  /**
   * This function is empty but necessary
   * because it is called during the rendering process of Leaflet v1.0.
   * @method _project
   */
  _project: function () {
  },

  /**
   * Update the path
   * This function is called during the rendering process of leaflet v1.0
   * @method _update
   */
  _update: function () {
    this._setPath();
  },

  /**
   * Sets the contents of the d-attribute in a path-element of an svg-file.
   * @method _setPath
   */
  _setPath: function () {
    this._path.setAttribute('d', this.getPathString());
  },

  /**
   * Set the default symbol.
   * @method setDefaultSymbol
   * @param symbol {Array} The corner points of the symbol.
   */
  setDefaultSymbol: function (symbol) {
    this._triSymbol = symbol;
    return this.redraw();
  },

  /**
   * Set the symbol for tracks with no heading.
   * @method setNoHeadingSymbol
   * @param symbol {Array} The corner points of the symbol.
   */
  setNoHeadingSymbol: function (symbol) {
    this._diaSymbol = symbol;
    return this.redraw();
  },

  /**
   * Set the symbol for tracks with shown silouette.
   * @method setSilouetteSymbol
   * @param symbol {Array} The corner points of the symbol.
   */
  setSilouetteSymbol: function (symbol) {
    this._silSymbol = symbol;
    return this.redraw();
  },

  setData: function (data) {
    var oldLatLng = this._latlng;
    this._latlng = L.latLng({lat: data.latitude, lng: data.longitude});
    this._speed = data.speed;
    this._course = data.course * Math.PI / 180;
    if (!!data.heading && data.heading > -1) {
      this._heading = data.heading * Math.PI / 180;
    } else {
      this._heading = this._course;
    }

    // if (this._id === '414301000') {
    //   console.log('set data:', this._course, this._heading);
    // }
    this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng});
    return this.redraw();
  },

  /**
   * Set latitude/longitude on the symbol.
   * @method setLatLng
   * @param latlng {LatLng} Position of the symbol on the map.
   */
  setLatLng: function (latlng) {
    var oldLatLng = this._latlng;
    this._latlng = L.latLng(latlng);
    this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng});
    return this.redraw();
  },

  /**
   * Set the speed shown in the symbol [m/s].
   * The leader-length is calculated via leaderTime.
   * @method setSpeed
   * @param speed {Number} The speed in [m/s].
   */
  setSpeed: function (speed) {
    this._speed = speed;
    return this.redraw();
  },

  /**
   * Sets the course over ground [rad].
   * The speed-leader points in this direction.
   * @method setCourse
   * @param course {Number} The course in radians.
   */
  setCourse: function (course) {
    this._course = course;
    return this.redraw();
  },

  /**
   * Sets the heading of the symbol [rad].
   * The heading rotates the symbol.
   * @method setHeading
   * @param course {Number} The heading in radians.
   */
  setHeading: function (heading) {
    this._heading = heading;
    return this.redraw();
  },

  getCourse: function() {
    return this._course;
  },

  getHeading: function() {
    return this._heading;
  },

  /**
   * Sets the leaderTime of the symbol [seconds].
   * @method setLeaderTime
   * @param leaderTime {Number} The leaderTime in seconds.
   */
  setLeaderTime: function (leaderTime) {
    this._leaderTime = leaderTime;
    return this.redraw();
  },

  /**
   * Sets the position offset of the silouette to the center of the symbol.
   * The array contains the refpoints from ITU R-REC-M.1371-4-201004 page 108
   * in sequence A,B,C,D.
   * @method setGPSRefPos
   * @param gpsRefPos {Array} The GPS offset from center.
   */
  setGPSRefPos: function (gpsRefPos) {
    if (gpsRefPos === undefined ||
      gpsRefPos.length < 4) {
      this._gpsRefPos = undefined;
    } else if (gpsRefPos[0] === 0 &&
      gpsRefPos[1] === 0 &&
      gpsRefPos[2] === 0 &&
      gpsRefPos[3] === 0) {
      this._gpsRefPos = undefined;
    } else {
      this._gpsRefPos = gpsRefPos;
    }
    return this.redraw();
  },

  /**
   * Returns the trackId.
   * @method getTrackId
   * @return {Number} The track id.
   */
  getTrackId: function () {
    return this._id;
  },

  _getLatSize: function () {
    return this._getLatSizeOf(this._size);
  },

  _getLngSize: function () {
    return this._getLngSizeOf(this._size);
  },

  _getLatSizeOf: function (value) {
    return (value / 40075017) * 360;
  },

  _getLngSizeOf: function (value) {
    return ((value / 40075017) * 360) / Math.cos((Math.PI / 180) * this._latlng.lat);
  },

  /**
   * Returns the bounding box of the symbol.
   * @method getBounds
   * @return {LatLngBounds} The bounding box.
   */
  getBounds: function () {
    var lngSize = this._getLngSize() / 2.0;
    var latSize = this._getLatSize() / 2.0;
    var latlng = this._latlng;
    return new L.LatLngBounds(
      [latlng.lat - latSize, latlng.lng - lngSize],
      [latlng.lat + latSize, latlng.lng + lngSize]);
  },

  /**
   * Returns the position of the symbol on the map.
   * @mathod getLatLng
   * @return {LatLng} The position object.
   */
  getLatLng: function () {
    return this._latlng;
  },

  focus: function() {
    // this._focus = true;
    // this.redraw();
  },

  blur: function() {
    // this._focus = false;
    // this.redraw();
  },

  //
  // Rotates the given point around the angle.
  // @method _rotate
  // @param point {Array} A point vector 2d.
  // @param angle {Number} Angle for rotation [rad].
  // @return The rotated vector 2d.
  //
  _rotate: function (point, angle) {
    var x = point[0];
    var y = point[1];
    var si_z = Math.sin(angle);
    var co_z = Math.cos(angle);
    var newX = x * co_z - y * si_z;
    var newY = x * si_z + y * co_z;
    return [newX, newY];
  },

  //
  // Rotates the given point-array around the angle.
  // @method _rotateAllPoints
  // @param points {Array} A point vector 2d.
  // @param angle {Number} Angle for rotation [rad].
  // @return The rotated vector-array 2d.
  //
  _rotateAllPoints: function (points, angle) {
    var result = [];
    for (var i = 0; i < points.length; i += 2) {
      var x = points[i + 0] * this._size;
      var y = points[i + 1] * this._size;
      var pt = this._rotate([x, y], angle);
      result.push(pt[0]);
      result.push(pt[1]);
    }
    return result;
  },

  _createLeaderViewPoints: function (angle) {
    var result = [];
    var leaderLength = this._speed * this._leaderTime;
    var leaderEndLng = this._latlng.lng + this._getLngSizeOf(leaderLength * Math.cos(angle));
    var leaderEndLat = this._latlng.lat + this._getLatSizeOf(leaderLength * Math.sin(angle));
    var endPoint = this._map.latLngToLayerPoint(L.latLng([leaderEndLat, leaderEndLng]));
    var startPoint = this._map.latLngToLayerPoint(this._latlng);
    return [startPoint.x, startPoint.y, endPoint.x, endPoint.y];
  },

  _transformAllPointsToView: function (points) {
    var result = [];
    var symbolViewCenter = this._map.latLngToLayerPoint(this._latlng);
    for (var i = 0; i < points.length; i += 2) {
      var x = symbolViewCenter.x + points[i + 0];
      var y = symbolViewCenter.y - points[i + 1];
      result.push(x);
      result.push(y);
    }
    return result;
  },

  _getRectPath: function () {
    return this._makeRectPath(21) + this._makeRectPath(19) + this._makeRectPath(17)
  },

  _makeRectPath: function (r) {
    // var r = 20;
    var high = 10;
    var result = '';
    var center = this._map.latLngToLayerPoint(this._latlng);
    var x0 = center.x - r, x1 = center.x + r, y0 = center.y - r, y1 = center.y + r;
    var pts = [
      x0, y0 + high,
      x0, y0,
      x0 + high, y0,
      x1 - high, y0,
      x1, y0,
      x1, y0 + high,
      x1, y1 - high,
      x1, y1,
      x1 - high, y1,
      x0 + high, y1,
      x0, y1,
      x0, y1 - high
    ];
    for (var i = 0; i < pts.length; i += 2) {
      if ((i + 1) % 3 < 2) {
        result += 'M ' + pts[i] + ' ' + pts[i + 1] + ' ';
        result += 'L ' + pts[i + 2] + ' ' + pts[i + 3] + ' ';
      }
    }
    // result += 'M ' + x0 + ' ' + (y0 + high) + ' ';
    // result += 'L ' + x0 + ' ' + y0 + ' ';
    // result += 'M ' + x0 + ' ' + y0 + ' ';
    // result += 'L ' + (x0 + high) + ' ' + y0 + ' ';
    return result
  },

  _createPathFromPoints: function (points) {
    var result;
    for (var i = 0; i < points.length; i += 2) {
      var x = points[i + 0];
      var y = points[i + 1];
      if (result === undefined)
        result = 'M ' + x + ' ' + y + ' ';
      else
        result += 'L ' + x + ' ' + y + ' ';
    }
    return result + ' Z';
  },

  _getViewAngleFromModel: function (modelAngle) {
    return Math.PI / 2.0 - modelAngle;
  },

  _createNoHeadingSymbolPathString: function () {
    var viewPoints = this._transformAllPointsToView(this._rotateAllPoints(this._diaSymbol, 0.0));
    var viewPath = this._createPathFromPoints(viewPoints);
    if (this._course !== undefined && this._speed !== undefined) {
      var courseAngle = this._getViewAngleFromModel(this._course);
      var leaderPoints = this._createLeaderViewPoints(courseAngle);
      viewPath += '' + this._createPathFromPoints(leaderPoints);
    }
    return viewPath;
  },

  _createWithHeadingSymbolPathString: function () {
    var headingAngle = this._getViewAngleFromModel(this._heading);
    var viewPoints = this._transformAllPointsToView(this._rotateAllPoints(this._triSymbol, headingAngle));
    var viewPath = this._createPathFromPoints(viewPoints);
    if (this._course !== undefined && this._speed !== undefined) {
      var courseAngle = this._getViewAngleFromModel(this._course);
      var leaderPoints = this._createLeaderViewPoints(courseAngle);
      viewPath += '' + this._createPathFromPoints(leaderPoints);
    }
    // if (this._id === '413440460') {
    //
    //
    //   console.log('path:', viewPath);
    // }
    return viewPath;
  },

  _resizeAndMovePoint: function (point, size, offset) {
    return [
      point[0] * size[0] + offset[0],
      point[1] * size[1] + offset[1]
    ];
  },

  _getSizeFromGPSRefPos: function () {
    return [
      this._gpsRefPos[0] + this._gpsRefPos[1],
      this._gpsRefPos[2] + this._gpsRefPos[3]
    ];
  },

  _getOffsetFromGPSRefPos: function () {
    return [
      -this._gpsRefPos[1],
      -this._gpsRefPos[3]
    ];
  },

  _transformSilouetteSymbol: function () {
    var headingAngle = this._getViewAngleFromModel(this._heading);
    var result = [];
    var size = this._getSizeFromGPSRefPos();
    var offset = this._getOffsetFromGPSRefPos();
    for (var i = 0; i < this._silSymbol.length; i += 2) {
      var pt = [
        this._silSymbol[i + 0],
        this._silSymbol[i + 1]
      ];
      pt = this._resizeAndMovePoint(pt, size, offset);
      pt = this._rotate(pt, headingAngle);
      var pointLng = this._latlng.lng + this._getLngSizeOf(pt[0]);
      var pointLat = this._latlng.lat + this._getLatSizeOf(pt[1]);
      var viewPoint = this._map.latLngToLayerPoint(L.latLng([pointLat, pointLng]));
      result.push(viewPoint.x);
      result.push(viewPoint.y);
    }
    return result;
  },

  _createSilouetteSymbolPathString: function () {
    var silouettePoints = this._transformSilouetteSymbol();
    var viewPath = this._createPathFromPoints(silouettePoints);
    if (this._course !== undefined && this._speed !== undefined) {
      var courseAngle = this._getViewAngleFromModel(this._course);
      var leaderPoints = this._createLeaderViewPoints(courseAngle);
      viewPath += '' + this._createPathFromPoints(leaderPoints);
    }
    return viewPath;
  },

  /**
   * Generates the symbol as SVG path string.
   * depending on zoomlevel or track heading different symbol types are generated.
   * @return {String} The symbol path string.
   */
  getPathString: function () {
    let pathString = '';
    if (this._heading === undefined || this._speed < 0.3) {
      pathString = this._createNoHeadingSymbolPathString();
    } else {
      if (this._gpsRefPos === undefined || this._map.getZoom() <= this._minSilouetteZoom) {
        pathString = this._createWithHeadingSymbolPathString();
      } else {
        pathString = this._createSilouetteSymbolPathString();
      }
    }

    if (this._focus) {
      pathString += this._getRectPath();
    }
    return pathString;
  }
});

/**
 * Factory function to create the symbol.
 * @method trackSymbol
 * @param latlng {LatLng} The position on the map.
 * @param options {Object} Additional options.
 */
L.trackSymbol = function (latlng, options) {
  return new L.TrackSymbol(latlng, options);
};

